using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading;
using Cfg.Exceptions;
using Cfg.Helpers;
using Path = System.IO.Path;

namespace Cfg
{
    public class Configuration
    {
		public IPathProvider RootPathProvider { get; set; }

        private readonly Dictionary<string, PropertyInfo> _properties = new Dictionary<string, PropertyInfo>();

        /// <summary>
        /// Configuration object type
        /// </summary>
        public readonly Type Type;

        /// <summary>
        /// Validation context
        /// </summary>
        public readonly string Context;

        /// <summary>
        /// Absolute path of root directory.
        /// </summary>
        private string RootPath
        {
            get { return RootPathProvider.GetRootPath(); }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <param name="context"></param>
        public Configuration(Type type, string context = null)
        {
            Type = type;
            Context = context;
            LoadProperties(Type);
			RootPathProvider = new PathProvider();
        }

        /// <summary>
        /// Tries to set all configuration values and collects all errors.
        /// </summary>
        /// <returns></returns>
        public ValidationError[] SetValues()
        {
            var errors = new List<ValidationError>();
            foreach (var prop in _properties)
            {
                try
                {
                    var value = GetFieldValue(prop.Key);
                    prop.Value.SetValue(null, value, null);
                }
                catch (Exception ex)
                {
                    errors.Add(new ValidationError(prop.Key, ex.Message));
                }
            }

            return errors.ToArray();
        }

		/*
        /// <summary>
        /// Get value from config file by key
        /// </summary>
        /// <param name="key"></param>
        /// <returns></returns>
        public object GetFieldValue(string key)
        {
            return GetFieldValue(key, _properties, Context);
        }
		*/

        /// <summary>
        /// Get value from config file by key.
        /// </summary>
        /// <param name="key">Key in config file</param>
        /// <returns></returns>
        public object GetFieldValue(string key)
        {
            var propInfo = _properties[key];

            var stringValue = ConfigurationManager.AppSettings[key];
            if (string.IsNullOrEmpty(stringValue))
            {
                var defaultValue = propInfo.GetDefaultValue();
                if (defaultValue != null)
                    return defaultValue;
            }

            if (propInfo.IsRequiredIn(Context) && string.IsNullOrEmpty(stringValue))
            {
                throw new ValueNotFoundException("Field is required, but value was not found");
            }

			var finalizedStringValue = FinalizeStringValue(stringValue);

			var propType = propInfo.PropertyType;

			if (propType == typeof(ConfigValues.Path))
			{
				bool createIfNotExists = propInfo.HasCreateDirectoryIfNotExists();
				bool throwIfNotExists = propInfo.HasCreateDirectoryIfNotExists();

				var absolutePath = ResolvePath(finalizedStringValue, createIfNotExists, throwIfNotExists);

                /*
				return new ConfigValues.Path
					{
						Absolute = absolutePath, 
						Relative = absolutePath.Replace(RootPath, "").TrimStart(Path.DirectorySeparatorChar),
						StringValue = stringValue, 
						FinalizedStringValue = finalizedStringValue
					};*/
			}

            if (propType == typeof(string))
            {
                bool createIfNotExists;
	            bool throwIfNotExists;
                if (propInfo.IsPath(out createIfNotExists, out throwIfNotExists))
                {
                    return ResolvePath(finalizedStringValue, createIfNotExists, throwIfNotExists);
                }
                return finalizedStringValue;
            }

            try
            {
				if (string.IsNullOrEmpty(finalizedStringValue))
					return GetDefault(propType);

                var value = Convert(finalizedStringValue, propType);
                if (propType.IsComparable())
                {
                    var maxValue = propInfo.GetMaxValue();
                    if (maxValue != null)
                    {
                        if (value.IsGreaterThan(maxValue))
                            throw new OutOfBoundsException(string.Format("{0} exceeds max value {1}", value, maxValue));
                    }
                    var minValue = propInfo.GetMinValue();
                    if (minValue != null)
                    {
                        if (value.IsLessThan(minValue))
                            throw new OutOfBoundsException(string.Format("{0} exceeds min value {1}", value, minValue));
                    }
                }

                return value;
            }
            catch (TargetInvocationException)
            {
                throw new InvalidFormatException(string.Format("Invalid format. {0} is not type of {1}", finalizedStringValue, propType));
            }
        }

		/// <summary>
		/// Finalize string value. Replaces all placeholders with actual values.
		/// </summary>
		/// <param name="stringValue"></param>
		/// <returns></returns>
		private string FinalizeStringValue(string stringValue)
	    {
			if (string.IsNullOrEmpty(stringValue))
				return stringValue;

			var regex = new Regex("(%[^%]*%)");
			var matches = regex.Matches(stringValue);
			foreach (Match match in matches)
			{
				var subkey = match.Value.Trim('%');
				//var subValue = (string)GetFieldValue(subkey, propertyInfos, context);

				string subValue;
				var o = GetFieldValue(subkey);
				if (o == null || o is string)
				{
					subValue = (string) o;
				}
				else
				{
					subValue = o.ToString();
				}

				stringValue = stringValue.Replace(match.Value, subValue);
			}

		    return stringValue;
	    }

	    /// <summary>
        /// Get default value for type.
        /// If type is value type, new instance will be returned.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
		public static object GetDefault(Type type)
		{
			if (type.IsValueType)
			{
				return Activator.CreateInstance(type);
			}
			return null;
		}

        /// <summary>
        /// Converts string value to given type
        /// </summary>
        /// <param name="stringValue"></param>
        /// <param name="to"></param>
        /// <returns></returns>
        private static object Convert(string stringValue, Type to)
        {
            if (to.IsArray)
            {
                var elementType = to.GetElementType();
                var split = stringValue.Split(AppSettings.Settings.ArrayDelimiters);

                var array = Array.CreateInstance(elementType, split.Length);
                for (var i = 0; i < split.Length; ++i)
                {
                    var itemValue = Convert(split[i].Trim(), elementType);
                    array.SetValue(itemValue, i);
                }

                return array;
            }

            if (to.IsGenericType && to.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                var elementType = Nullable.GetUnderlyingType(to);

                if (string.IsNullOrEmpty(stringValue))
                {
                    return null;
                }

                return Convert(stringValue, elementType);
            }

            if (to.IsEnum)
            {
                return Enum.Parse(to, stringValue);
            }
			/*
			if (to == typeof(ConfigValueBase))
			{
				
			}
			*/
            var methodName = string.Format("To{0}", to.Name);

            var convertMethod = typeof(Convert).GetMethod(methodName, new[] { typeof(string), typeof(IFormatProvider) });
            var ci = AppSettings.Settings.Culture ?? Thread.CurrentThread.CurrentCulture;
            return convertMethod.Invoke(null, new object[] { stringValue, ci });
        }

	    /// <summary>
	    /// Get absolute path.
	    /// </summary>
	    /// <param name="path">Relative or absolute path</param>
	    /// <param name="createIfNotExists">if set true, tries to create directory if not exists. Otherwise <see cref="DirectoryNotFoundException" /> will be thrown.</param>
		/// <param name="throwIfNotExists">if set false ignores <see cref="DirectoryNotFoundException" /></param>
	    /// <returns></returns>
	    public string ResolvePath(string path, bool createIfNotExists, bool throwIfNotExists)
        {
            if (string.IsNullOrEmpty(path))
                return null;

            if (path.StartsWith("~/"))
                path = path.Substring(2);

            if (!Path.IsPathRooted(path))
            {
                path = Path.Combine(RootPath, path);
            }

            if (!Directory.Exists(path))
            {
                if (createIfNotExists)
                {
                    Directory.CreateDirectory(path);
                }
				else if (throwIfNotExists)
                {
					throw new DirectoryNotFoundException();
                }
            }

            return path;
        }

        /// <summary>
        /// Load all public static properties with getter and setter method from given type.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="prefix">Prefix that will be added to property key</param>
        /// <returns></returns>
        private void LoadProperties(Type type, string prefix = null)
        {
            // todo - avoid circular reference
            var properties = type.GetProperties(BindingFlags.Public | BindingFlags.Static | (BindingFlags.SetProperty & BindingFlags.GetProperty));
            
            foreach (var propertyInfo in properties)
            {
				if (!propertyInfo.CanWrite)
					continue;

                string key = prefix == null ? propertyInfo.Name : string.Format("{0}.{1}", prefix, propertyInfo.Name);
                _properties[key] = propertyInfo;
            }

            var subTypes = type.GetNestedTypes(BindingFlags.Instance | BindingFlags.Public);
            foreach (var subType in subTypes)
            {
                LoadProperties(subType, prefix == null ? subType.Name : string.Format("{0}.{1}", prefix, subType.Name));
            }
        }
    }
}